/** @file ar_proc.c
  *
  * @brief This file contains the proc fs function calls
  *
  *  Copyright (C) 2024, Artosyn Technologies. ALL RIGHTS RESERVED.
  *
  *  This Information is proprietary to Artosyn Technologies and MAY NOT
  *  be copied by any method or incorporated into another program without
  *  the express written consent of Artosyn. This Information or any portion
  *  thereof remains the property of Artosyn. The Information contained herein
  *  is believed to be accurate and Artosyn assumes no responsibility or
  *  liability for its use in any way and conveys no license or title under
  *  any patent or copyright and makes no representation or warranty that this
  *  Information is free from patent or copyright infringement.
  *
  */
#include <linux/proc_fs.h>
#include <linux/types.h>
#include "utils/utils.h"
#include "oal_main.h"
#include "control/ar_proc.h"
#include "net/net_dev.h" /** TBD: register the stats get callback */

/** proc diretory root */
#define PROC_DIR NULL

/** Proc mdtop directory entry */
static struct proc_dir_entry *proc_artosyn;

/** sdio rx TP test mode */
extern bool sdio_rx_tp_test;
/** sdio tx TP test mode */
extern bool sdio_tx_tp_test;
/** sdio tx TP test buf len */
extern int sdio_tx_tp_test_len;

/** proc data structure */
struct proc_data {
    /** Read length */
    int rdlen;
    /** Read buffer */
    char *rdbuf;
    /** Write length */
    int wrlen;
    /** Maximum write length */
    int maxwrlen;
    /** Write buffer */
    char *wrbuf;
    /** OAL main structure */
    void *handle;
    void (*on_close) (struct inode *, struct file *);
};

/** Default file permission */
#define DEFAULT_FILE_PERM          0644

/** Dtop device offset */
#define OFFSET_ARTOSYN_DEV         0x01

/** Dtop dev proc name */
#define ARTOSYN_PROC_NAME         "ar_drv"

/** Show integer */
#define SHOW_INT        0x10
/** Show hex */
#define SHOW_HEX        0x20
/** Show string */
#define SHOW_STRING     0x40

/** Device size */
#define item_dev_size(n) (sizeof((ar_dev_t *)0)->n)
/** Device address */
#define item_dev_addr(n) ((t_ptr) &((ar_dev_t *)0)->n)

static struct item_data dbg_items[] = {
    /** echo cmd
     */
    {"echo_cmd", item_dev_size(echo_cmd), (t_ptr)0, (loff_t)item_dev_addr(echo_cmd),
     OFFSET_ARTOSYN_DEV | SHOW_STRING, 0x0},
    /** Debug log level
     */
    {"dbg_log_level", item_dev_size(dbg_log_level), (t_ptr)0, (loff_t)item_dev_addr(dbg_log_level),
     OFFSET_ARTOSYN_DEV | SHOW_HEX, DEFAULT_DEBUG_LVL},
    /** sdio tx TP test buf len
     */
    {"sdio_tx_tp_test_len", item_dev_size(sdio_tx_tp_test_len), (t_ptr)0, (loff_t)item_dev_addr(sdio_tx_tp_test_len),
     OFFSET_ARTOSYN_DEV | SHOW_INT, 4000},
    /** sdio tx TP test mode
     */
    {"sdio_tx_tp_test", item_dev_size(sdio_tx_tp_test), (t_ptr)0, (loff_t)item_dev_addr(sdio_tx_tp_test),
     OFFSET_ARTOSYN_DEV | SHOW_INT, 0},
    /** sdio rx TP test mode
     */
    {"sdio_rx_tp_test", item_dev_size(sdio_rx_tp_test), (t_ptr)0, (loff_t)item_dev_addr(sdio_rx_tp_test),
     OFFSET_ARTOSYN_DEV | SHOW_INT, 0}
};

/**
 *  @brief convert string to number
 *
 *  @param     pointer to numbered string
 *  @return    converted number from string s
 */
int
string_to_number(char *s)
{
    int r = 0;
    int base = 0;
    int pn = 1;

    if (strncmp(s, "-", 1) == 0) {
        pn = -1;
        s++;
    }
    if ((strncmp(s, "0x", 2) == 0) || (strncmp(s, "0X", 2) == 0)) {
        base = 16;
        s += 2;
    } else
        base = 10;

    for (; *s != 0; s++) {
        if ((*s >= '0') && (*s <= '9'))
            r = (r * base) + (*s - '0');
        else if ((*s >= 'A') && (*s <= 'F'))
            r = (r * base) + (*s - 'A' + 10);
        else if ((*s >= 'a') && (*s <= 'f'))
            r = (r * base) + (*s - 'a' + 10);
        else
            break;
    }

    return r * pn;
}

/**
 *  @brief This function handle generic proc file close
 *
 *  @param inode   A pointer to inode structure
 *  @param file    A pointer to file structure
 *  @return    0
 */
static int
proc_close(struct inode *inode, struct file *file)
{
    struct proc_data *pdata = file->private_data;
    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);
    if (pdata) {
        if (pdata->on_close != NULL)
            pdata->on_close(inode, file);
        kfree(pdata->rdbuf);
        kfree(pdata->wrbuf);
        kfree(pdata);
    }
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return 0;
}

/**
 *  @brief This function handle generic proc file read
 *
 *  @param file    A pointer to file structure
 *  @param buffer  A pointer to output buffer
 *  @param len     number of byte to read
 *  @param offset  A pointer to offset of file
 *  @return        number of output data
 */
static ssize_t
proc_read(struct file *file, char __user * buffer, size_t len, loff_t * offset)
{
    int ret = 0;
    loff_t pos = *offset;
    char info[512] = {0};

    if(pos > 0){
        return 0;
    }

    /** FIXME: */
    // ar_sdio_stats_str(info, sizeof(info));
    sprintf(info, "Hello world!\n");

    if (copy_to_user(buffer, info, strlen(info) + 1))
        return -EFAULT;
    *offset = 1;
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return strlen(info) + 1;
}

/**
 *  @brief This function handle generic proc file write
 *
 *  @param file    A pointer to file structure
 *  @param buffer  A pointer to input buffer
 *  @param len     number of byte to write
 *  @param offset  A pointer to offset of file
 *  @return        number of input data
 */
static ssize_t
proc_write(struct file *file,
       const char __user * buffer, size_t len, loff_t * offset)
{
    loff_t pos = *offset;
    int config_data = 0;
    struct proc_data *pdata = (struct proc_data *)file->private_data;
    char *line = NULL;
    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);
    if (!pdata->wrbuf || (pos < 0))
        return -EINVAL;
    if (pos >= pdata->maxwrlen)
        return 0;
    if (len > pdata->maxwrlen - pos)
        len = pdata->maxwrlen - pos;
    if (copy_from_user(pdata->wrbuf + pos, buffer, len))
        return -EFAULT;

    /* Config operation */
    if (!strncmp(pdata->wrbuf + pos, "echo_cmd=", strlen("echo_cmd="))) {
        line = pdata->wrbuf + pos;
        line += strlen("echo_cmd") + 1;
        AR_DBG(DM_PROC, DL_MSG, "echo_cmd=%s\n", line);
    }
    /* TBD: pm issue
    else if (!strncmp(pdata->wrbuf + pos, "autopm=", strlen("autopm="))) {
        line = pdata->wrbuf + pos;
        line += strlen("autopm") + 1;
        config_data = string_to_number(line);
        AR_DBG(DM_PROC, DL_MSG, "Config autopm=%d\n", config_data);
        if (config_data){
            ar_usb_enable_autosuspend();
        } else {
            ar_usb_disable_autosuspend();
        }
    }
    */
    else if (!strncmp(pdata->wrbuf + pos, "dbg_log_level=", strlen("dbg_log_level="))) {
        line = pdata->wrbuf + pos;
        line += strlen("dbg_log_level") + 1;
        config_data = string_to_number(line);
        AR_DBG(DM_PROC, DL_MSG, "Request dbg_log_level=0x%x\n", (unsigned int)config_data);
        ar_dbg_lv = (unsigned int)config_data;
    }
    else if (!strncmp(pdata->wrbuf + pos, "sdio_tx_tp_test_len=", strlen("sdio_tx_tp_test_len="))) {
        line = pdata->wrbuf + pos;
        line += strlen("sdio_tx_tp_test_len") + 1;
        config_data = string_to_number(line);
        AR_DBG(DM_PROC, DL_MSG, "Request sdio_tx_tp_test_len=0x%x\n", (unsigned int)config_data);
        sdio_tx_tp_test_len = (unsigned int)config_data;
    }
    else if (!strncmp(pdata->wrbuf + pos, "sdio_tx_tp_test=", strlen("sdio_tx_tp_test="))) {
        line = pdata->wrbuf + pos;
        line += strlen("sdio_tx_tp_test") + 1;
        config_data = string_to_number(line);
        AR_DBG(DM_PROC, DL_MSG, "Request sdio_tx_tp_test=0x%x\n", (unsigned int)config_data);
        sdio_tx_tp_test = (unsigned int)config_data;
    }
    else if (!strncmp(pdata->wrbuf + pos, "sdio_rx_tp_test=", strlen("sdio_rx_tp_test="))) {
        line = pdata->wrbuf + pos;
        line += strlen("sdio_rx_tp_test") + 1;
        config_data = string_to_number(line);
        AR_DBG(DM_PROC, DL_MSG, "Request sdio_rx_tp_test=0x%x\n", (unsigned int)config_data);
        sdio_rx_tp_test = (unsigned int)config_data;
    }

    if (pos + len > pdata->wrlen)
        pdata->wrlen = len + file->f_pos;
    *offset = pos + len;
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return len;
}

/**
 *  @brief This function handle the generic file close
 *
 *  @param inode   A pointer to inode structure
 *  @param file    A pointer to file structure
 *  @return        N/A
 */
static void
proc_on_close(struct inode *inode, struct file *file)
{
#if KERNEL_VERSION(5, 16, 0) <= LINUX_VERSION_CODE
    struct proc_private_data *priv = pde_data(inode);
#else
    struct proc_private_data *priv = PDE_DATA(inode);
#endif
    struct proc_data *pdata = file->private_data;
    char *line;
    int i;
    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);
    if (!pdata->wrlen)
        return;
    line = pdata->wrbuf;
    while (line[0]) {
        for (i = 0; i < priv->num_items; i++) {
            if (!strncmp
                (line, priv->pdata[i].name,
                 strlen(priv->pdata[i].name))) {
                line += strlen(priv->pdata[i].name) + 1;
                if (priv->pdata[i].size == 1)
                    *((u8 *)priv->pdata[i].addr) =
                        (u8)string_to_number(line);
                else if (priv->pdata[i].size == 2)
                    *((u16 *) priv->pdata[i].addr) =
                        (u16) string_to_number(line);
                else if (priv->pdata[i].size == 4)
                    *((u32 *)priv->pdata[i].addr) =
                        (u32)string_to_number(line);
            }
        }
        while (line[0] && line[0] != '\n')
            line++;
        if (line[0])
            line++;
    }
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return;
}

/**
 *  @brief This function handle the generic file open
 *
 *  @param inode   A pointer to inode structure
 *  @param file    A pointer to file structure
 *  @return    0 or other error no.
 */
static int
proc_open(struct inode *inode, struct file *file)
{
#if KERNEL_VERSION(5, 16, 0) <= LINUX_VERSION_CODE
    struct proc_private_data *priv = pde_data(inode);
#else
    struct proc_private_data *priv = PDE_DATA(inode);
#endif
    struct proc_data *pdata;
    int i;
    char *p;
    u32 val = 0;
    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);
    file->private_data = kzalloc(sizeof(struct proc_data), GFP_KERNEL);
    if (file->private_data == NULL) {
        AR_DBG(DM_PROC, DL_ERR, "Can not alloc mem for proc_data\n");
        AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
        return -ENOMEM;
    }
    pdata = (struct proc_data *)file->private_data;
    pdata->handle = priv->handle;
    pdata->rdbuf = kmalloc(priv->bufsize, GFP_KERNEL);
    if (pdata->rdbuf == NULL) {
        AR_DBG(DM_PROC, DL_ERR, "Can not alloc mem for rdbuf\n");
        kfree(file->private_data);
        AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
        return -ENOMEM;
    }
    if (priv->fileflag == DEFAULT_FILE_PERM) {
        pdata->wrbuf = kzalloc(priv->bufsize, GFP_KERNEL);
        if (pdata->wrbuf == NULL) {
            AR_DBG(DM_PROC, DL_ERR, "Can not alloc mem for wrbuf\n");
            kfree(pdata->rdbuf);
            kfree(file->private_data);
            return -ENOMEM;
        }
        pdata->maxwrlen = priv->bufsize;
        pdata->on_close = proc_on_close;
    }
    p = pdata->rdbuf;
    for (i = 0; i < priv->num_items; i++) {
        if (priv->pdata[i].size == 1)
            val = *((u8 *)priv->pdata[i].addr);
        else if (priv->pdata[i].size == 2)
            val = *((u16 *) priv->pdata[i].addr);
        else if (priv->pdata[i].size == 4)
            val = *((u32 *)priv->pdata[i].addr);

        if (priv->pdata[i].flag & SHOW_INT)
            p += sprintf(p, "%s=%d\n", priv->pdata[i].name, val);
        else if (priv->pdata[i].flag & SHOW_HEX)
            p += sprintf(p, "%s=0x%x\n", priv->pdata[i].name, val);
        else if (priv->pdata[i].flag & SHOW_STRING) {
            p += sprintf(p, "%s=%s\n", priv->pdata[i].name,
                     (char *)priv->pdata[i].addr);
        }
    }
    pdata->rdlen = strlen(pdata->rdbuf);
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return 0;
}

/** Proc Read-Write ops */
static const struct file_operations proc_rw_ops = {
    .read = proc_read,
    .write = proc_write,
    .open = proc_open,
    .release = proc_close
};

static struct proc_private_data proc_files[] = {
    {"dbg", DEFAULT_FILE_PERM, 512,
     sizeof(dbg_items) / sizeof(dbg_items[0]), &dbg_items[0], NULL,
     &proc_rw_ops}
};

/**
 *  @brief This function initializes proc entry
 *
 *  @param handle_p  A pointer to oal_handle structure
 *  @param name      A pointer to name
 *
 *  @return    0 or -1
 */
int
ar_proc_init(void *handle_p, char *name)
{
    int ret = 0;
    struct proc_dir_entry *entry;
    oal_handle *handle = (oal_handle*)handle_p;
    int i, j;
    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);

    if (!handle) {
        ret = -1;
        goto done;
    }

    if (proc_artosyn) {
        AR_DBG(DM_PROC, DL_MSG, "Create proc node %s\n", name);

        handle->dev_proc.proc_entry = proc_mkdir(name, proc_artosyn);
        if (!handle->dev_proc.proc_entry) {
            AR_DBG(DM_PROC, DL_ERR, "Could not mkdir %s!\n", name);
            ret = -2;
            goto done;
        }
        strcpy(handle->ar_dev.name, name);

        handle->dev_proc.pfiles = kmalloc(sizeof(proc_files), GFP_ATOMIC);
        if (!handle->dev_proc.pfiles) {
            AR_DBG(DM_PROC, DL_ERR,
                   "Could not alloc memory for pfile!\n");
            ret = -3;
            goto done;
        }

        memcpy((u8 *)handle->dev_proc.pfiles, (u8 *)proc_files, sizeof(proc_files));
        handle->dev_proc.num_proc_files = ARRAY_SIZE(proc_files);

        for (j = 0; j < handle->dev_proc.num_proc_files; j++)
            handle->dev_proc.pfiles[j].pdata = NULL;

        for (j = 0; j < handle->dev_proc.num_proc_files; j++) {
            handle->dev_proc.pfiles[j].pdata =
                kmalloc(handle->dev_proc.pfiles[j].num_items * sizeof(struct item_data), GFP_ATOMIC);
            if (!handle->dev_proc.pfiles[j].pdata) {
                AR_DBG(DM_PROC, DL_ERR,
                       "Could not alloc memory for pdata!\n");
                ret = -4;
                goto done;
            }

            memcpy((u8 *)handle->dev_proc.pfiles[j].pdata,
                   (u8 *)proc_files[j].pdata,
                   handle->dev_proc.pfiles[j].num_items * sizeof(struct item_data));

            for (i = 0; i < handle->dev_proc.pfiles[j].num_items;i++) {
                if (handle->dev_proc.pfiles[j].pdata[i].flag & OFFSET_ARTOSYN_DEV) {
                    handle->dev_proc.pfiles[j].pdata[i].addr =
                        handle->dev_proc.pfiles[j].pdata[i].offset + (t_ptr)&handle->ar_dev;
                    *((u32*)handle->dev_proc.pfiles[j].pdata[i].addr) = handle->dev_proc.pfiles[j].pdata[i].value;
                }
            }
            handle->dev_proc.pfiles[j].handle = handle;

            entry = proc_create_data(proc_files[j].name,
                         S_IFREG | proc_files[j].
                         fileflag,
                         handle->dev_proc.proc_entry,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 0, 0)
                         (const struct proc_ops *)proc_files[j].fops,
#else
                         proc_files[j].fops,
#endif
                         &handle->dev_proc.pfiles[j]);
            if (entry == NULL)
                AR_DBG(DM_PROC, DL_MSG, "Fail to create proc %s\n",
                       proc_files[j].name);
        }
    }
done:
    if (ret == -1) {
        if (handle->dev_proc.proc_entry) {
            remove_proc_entry(name, proc_artosyn);
            handle->dev_proc.proc_entry = NULL;
        }
        if (handle->dev_proc.pfiles) {
            for (j = 0; j < handle->dev_proc.num_proc_files; j++) {
                if (handle->dev_proc.pfiles[j].pdata) {
                    kfree(handle->dev_proc.pfiles[j].pdata);
                    handle->dev_proc.pfiles[j].pdata = NULL;
                }
            }
            kfree(handle->dev_proc.pfiles);
            handle->dev_proc.pfiles = NULL;
        }
    }
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return ret;
}

/**
 *  @brief This function removes proc interface
 *
 *  @param handle_p    A pointer to oal_handle structure
 *  @return    N/A
 */
void
ar_proc_remove(void *handle_p)
{
    int j;
    oal_handle *handle = (oal_handle*)handle_p;

    AR_DBG(DM_PROC, DL_INF, AR_FN_ENTRY_STR);

    if (!handle) {
        return;
    }

    AR_DBG(DM_PROC, DL_MSG, "Remove Proc Interface\n");
    if (proc_artosyn) {
        if (!handle->dev_proc.proc_entry)
            return;
        for (j = 0; j < ARRAY_SIZE(proc_files); j++) {
            remove_proc_entry(proc_files[j].name, handle->dev_proc.proc_entry);
        }
        remove_proc_entry(handle->ar_dev.name, proc_artosyn);
        handle->dev_proc.proc_entry = NULL;

        if (handle->dev_proc.pfiles) {
            for (j = 0;j < handle->dev_proc.num_proc_files;j++) {
                if (handle->dev_proc.pfiles[j].pdata) {
                    kfree(handle->dev_proc.pfiles[j].pdata);
                    handle->dev_proc.pfiles[j].pdata = NULL;
                }
            }
            kfree(handle->dev_proc.pfiles);
            handle->dev_proc.pfiles = NULL;
        }
    }
    AR_DBG(DM_PROC, DL_INF, AR_FN_LEAVE_STR);
    return;
}

/**
 *  @brief This function creates proc interface
 *  directory structure
 *
 *  @return        0 or -1
 */
int
ar_root_proc_init(char* proc_dir_name)
{
    AR_DBG(DM_PROC, DL_MSG, "Create Proc Interface\n");
    if (proc_dir_name){
        proc_artosyn = proc_mkdir(proc_dir_name, PROC_DIR);
    }
    else{
        proc_artosyn = proc_mkdir(ARTOSYN_PROC_NAME, PROC_DIR);
    }

    if (!proc_artosyn) {
        AR_DBG(DM_PROC, DL_ERR, "Cannot create proc interface\n");
        return -1;
    }
    return 0;
}

/**
 *  @brief This function removes proc interface
 *  directory structure
 *
 *  @return        0
 */
int
ar_root_proc_remove(char* proc_dir_name)
{
    if (proc_dir_name){
        remove_proc_entry(proc_dir_name, PROC_DIR);
    }
    else{
        remove_proc_entry(ARTOSYN_PROC_NAME, PROC_DIR);
    }
    proc_artosyn = NULL;
    return 0;
}
